﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Axial
{
	public abstract class JSCode { }
	public class JSStatement : JSCode
	{
		public string Statement { get; private set; }

		public JSStatement(string s)
		{
			Statement = s;
		}

		public override string ToString()
		{
			return Statement;
		}
	}
	public class JSBlock : JSCode
	{
		public List<JSCode> Statements { get; private set; }
		public string Header { get; private set; }
		public bool ForceBraces { get; private set; }

		public JSBlock(string header, JSWriter code) : this(header, code, false)
		{
		}

		public JSBlock(string header, JSWriter code, bool forceBraces)
		{
			Header = header;
			Statements = new List<JSCode>();
			ForceBraces = forceBraces;
			AddBracedBlock(code);
		}

		private void AddBracedBlock(JSWriter writer)
		{
			if (ForceBraces || writer.Count > 1)
				Statements.Add(new JSStatement("{\r\n"));
			foreach (JSCode code in writer)
				if (code is JSStatement)
					Statements.Add(new JSStatement("\t" + ((JSStatement)code).Statement));
				else if (code is JSBlock)
				{
					JSBlock block = (JSBlock)code;
					Statements.Add(new JSStatement("\t" + block.Header + "\r\n"));
					Statements.AddRange(block.Statements.Select(s => (JSCode)new JSStatement("\t" + ((JSStatement)s).Statement)));
				}
			if (ForceBraces || writer.Count > 1)
				Statements.Add(new JSStatement("}\r\n"));
		}

		public override string ToString()
		{
			return Header + "\r\n" + string.Join("", Statements.Select(c => c.ToString()).ToArray());
		}
	}

	public class JSWriter : List<JSCode>
	{
		public JSWriter() { }

		public void AddStatement(string statement)
		{
			Add(new JSStatement(statement + ";\r\n"));
		}
		public void AddStatements(IEnumerable<string> ss)
		{
			AddRange(ss.Select(s => (JSCode)new JSStatement(s + ";\r\n")));
		}
		public void AddStatements(JSWriter writer)
		{
			AddWriter(writer);
		}
		public void AddWriter(JSWriter writer)
		{
			AddRange(writer);
		}
		public void InsertWriter(JSWriter writer)
		{
			InsertRange(0, writer);
		}

		public void AddWhileBlock(string condition, JSWriter innerCode)
		{
			Add(new JSBlock("while (" + condition + ")", innerCode));
		}
		public void Add(WhileCondition whileBlock)
		{
			AddWhileBlock(whileBlock.Condition, whileBlock.CodeSegment.ToJSWriter());
		}
		public void AddIfBlock(string condition, JSWriter trueCode, JSWriter falseCode)
		{
			Add(new JSBlock("if (" + condition + ")", trueCode));
			if (falseCode.Any())
				Add(new JSBlock("else", falseCode));
		}
		public void Add(IfCondition ifBlock)
		{
			AddIfBlock(ifBlock.Condition, ifBlock.TrueSegment.ToJSWriter(), ifBlock.FalseSegment.ToJSWriter());
		}

		public override string ToString()
		{
			return string.Join("", this.Select(c => c.ToString()).ToArray());
		}

		// javascript class semantics taken from http://phrogz.net/js/Classes/OOPinJS.html
		public void AddPrivateFunction(string name, int argCount, JSWriter code)
		{
			var paramStr = string.Join(", ", (from i in Enumerable.Range(0, argCount) select "arg" + i).ToArray());
			Add(new JSBlock(string.Format("function {0}({1})", SanitizeIdentifier(name), paramStr), code, true));
		}
		public void AddPrivateFunction(string name, string[] parameterNames, JSWriter code)
		{
			var paramStr = string.Join(", ", parameterNames);
			Add(new JSBlock(string.Format("function {0}({1})", SanitizeIdentifier(name), paramStr), code, true));
		}
		public void AddPublicFunction(string name, int argCount, JSWriter code)
		{
			var paramStr = string.Join(", ", (from i in Enumerable.Range(0, argCount) select "arg" + i).ToArray());
			Add(new JSBlock(string.Format("this.{0} = function({1})", SanitizeIdentifier(name), paramStr), code, true));
		}
		public void AddAnonymousFunction(int argCount, JSWriter code)
		{
			var paramStr = string.Join(", ", (from i in Enumerable.Range(0, argCount) select "arg" + i).ToArray());
			Add(new JSBlock(string.Format("function({0})", paramStr), code, true));
		}
		public void AddPublicField(string name, string defaultValue)
		{
			Add(new JSStatement(string.Format("this.{0} = {1};\r\n", SanitizeIdentifier(name), defaultValue)));
		}
		public void AddPrivateField(string name, string defaultValue)
		{
			Add(new JSStatement(string.Format("var {0} = {1};\r\n", SanitizeIdentifier(name), defaultValue)));
		}
		public void AddStaticField(string name, string defaultValue)
		{
			Add(new JSStatement(string.Format("this.constructor.{0} = {1};\r\n", SanitizeIdentifier(name), defaultValue)));
		}
		public void AddConstructor(Type[] paramTypes, JSWriter jsWriter)
		{
			string lengthTest = "arguments.length == " + paramTypes.Length;
			string typeTest = "";
			if (paramTypes.Length > 0)
			{
				List<string> typeTests = new List<string>();
				for (int i = 0; i < paramTypes.Length; ++i)
				{
					Type t = paramTypes[i];
					string jsType = "object";
					if (t == typeof(int)) jsType = "number";
					if (t == typeof(double)) jsType = "number";
					if (t == typeof(float)) jsType = "number";
					if (t == typeof(string)) jsType = "string";
					typeTests.Add(string.Format("typeof arguments[{0}] == \"{1}\"", i, jsType));
				}
				typeTest = " && " + string.Join(" && ", typeTests.ToArray());
			}
			Add(new JSBlock("if (" + lengthTest + typeTest + ")", jsWriter));
		}
		public void AddBlankLine()
		{
			Add(new JSStatement("\r\n"));
		}

		public static string SanitizeIdentifier(string identifier)
		{
			return identifier.Replace("<", "").Replace(">", "");
		}
	}
}
